/*
 * Copyright (c) 2011-2021, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baomidou.mybatisplus.core.toolkit;

import java.sql.Timestamp;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 高并发场景下System.currentTimeMillis()的性能问题的优化
 *
 * <p>System.currentTimeMillis()的调用比new一个普通对象要耗时的多（具体耗时高出多少我还没测试过，有人说是100倍左右）</p>
 * <p>System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道</p>
 * <p>后台定时更新时钟，JVM退出时，线程自动回收</p>
 * <p>10亿：43410,206,210.72815533980582%</p>
 * <p>1亿：4699,29,162.0344827586207%</p>
 * <p>1000万：480,12,40.0%</p>
 * <p>100万：50,10,5.0%</p>
 *
 * 原理：单个调度线程来按毫秒更新时间戳，相当于维护一个全局缓存。其他线程取时间戳时相当于从内存取，不会再造成时钟资源的争用，代价就是牺牲了一些精确度
 *
 * 创建一个单例的对象，里面维护一个单线程的调度器，在固定的时间段，默认是1毫秒，获取系统时间并赋值给该对象的一个变量，相当于在内存当中维护了一个全局时间戳。
 * 高并发情况下，只需要调用这个缓存就行。优点是解决了System.currentTimeMillis()的性能问题，缺点是时间不太准确，获取的缓存时间肯定比系统时间有延迟1毫秒。
 *
 * 这里这个系统缓存时间设计有很多借鉴的优点：
 *
 *    1.单例模式  该类SystemClock里面维护了一个静态内部类InstanceHolder，里面有一个静态变量，就是实例化SystemClock对象。然后外部使用now（）
 *    调用，JVM启动时候，默认就创建了该对象。
 *    2.使用原子类AtomicLong做时间戳，保证多线程情况下的赋值是原子操作，now.set(System.currentTimeMillis())。
 *    3.使用Executors.newSingleThreadScheduledExecutor创建单一线程，将newSingleThreadScheduledExecutor创建单一线程特性发挥的非常好，
 *    使用Executors.scheduleAtFixedRate()方法，按照固定的时间频率来调度。
 */
public class SystemClock {

    private  volatile long period;
    private final AtomicLong now;

    private SystemClock(long period) {
        this.period = period;
        this.now = new AtomicLong(System.currentTimeMillis());
        scheduleClockUpdating();
    }

    private static SystemClock instance() {
        return InstanceHolder.INSTANCE;
    }

    public static long now() {
        return instance().currentTimeMillis();
    }

    public static String nowDate() {
        return new Timestamp(instance().currentTimeMillis()).toString();
    }

    private void scheduleClockUpdating() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "System Clock");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
    }

    private long currentTimeMillis() {
        return now.get();
    }

    private static class InstanceHolder {
        public static final SystemClock INSTANCE = new SystemClock(1);
    }
}
